using System;
using System.Collections.Generic;
using System.Text;

namespace Toenda.Foundation.Data {
	/// <summary>
	/// Defines the default operator to combine the several conditions. At
	/// the moment there are no other operators than OR and AND which makes
	/// sense.
	/// </summary>
	public enum QueryOperator {
		/// <summary>
		/// Query operator
		/// </summary>
		OR,
		/// <summary>
		/// Query operator to link search words
		/// </summary>
		AND
	}

	/// <summary>
	/// What type of query should be returned - one which works with Like %%
	/// or one which uses the DIFFERENCE().
	/// </summary>
	public enum QueryType {
		/// <summary>
		/// ...
		/// </summary>
		Like,
		/// <summary>
		/// ...
		/// </summary>
		Difference
	}

	/// <summary>
	/// This class offers in fact the Parse method which parses a query to
	/// a usable sql where condition statement
	/// </summary>
	public class QueryParser {
		#region "NESTED TYPES"
		// ---------------------------------------------------
		// NESTED TYPES
		// ---------------------------------------------------
		#endregion

		#region "INTERNAL VARIABLES"
		// ---------------------------------------------------
		// INTERNAL VARIABLES
		// ---------------------------------------------------

		private List<string> _searchTerms;
		private QueryOperator _defaultOperator = QueryOperator.OR;
		private QueryType _queryType = QueryType.Like;
		private int _minDifference = 3;
		
		private const string ExcludedCharacters = "AND OR NOT ( )";

		private List<string> ExcludedCharacterList = new List<string>() {
			"AND", "OR", "NOT", "(", ")"
		};

		#endregion

		#region "CONSTRUCTORS"
		// ---------------------------------------------------
		// CONSTRUCTORS
		// ---------------------------------------------------

		/// <summary>
		/// Initializes a new instance of the <see cref="QueryParser"/> class.
		/// </summary>
		public QueryParser() {
			this._searchTerms = new List<string>();
		}

		#endregion

		#region "INTERFACE IMPLEMENTATIONS"
		// ---------------------------------------------------
		// INTERFACE IMPLEMENTATIONS
		// ---------------------------------------------------
		#endregion

		#region "PROPERTIES"
		// ---------------------------------------------------
		// PROPERTIES
		// ---------------------------------------------------

		/// <summary>
		/// Default Operator
		/// </summary>
		public QueryOperator DefaultOperator {
			get { return this._defaultOperator; }
			set { this._defaultOperator = value; }
		}

		/// <summary>
		/// Gets or sets the type of the query.
		/// </summary>
		/// <value>The type of the query.</value>
		public QueryType QueryType {
			get { return this._queryType; }
			set { this._queryType = value; }
		}

		/// <summary>
		/// Gets or sets the min difference.
		/// </summary>
		/// <value>The min difference.</value>
		public int MinDifference {
			get { return this._minDifference; }
			set { this._minDifference = value; }
		}

		#endregion

		#region "EVENTS"
		// ---------------------------------------------------
		// EVENTS
		// ---------------------------------------------------
		#endregion

		#region "PRIVATE MEMBERS"
		// ---------------------------------------------------
		// PRIVATE MEMBERS
		// ---------------------------------------------------

		/// <summary>
		/// Buffers an operator or a term to this._searchTerms list
		/// </summary>
		/// <param name="term">Contains the string which has to be added.</param>
		/// <param name="isTerm">Define whether the string is a term or an operator.</param>
		private void _AddToSearchTerms(string term, bool isTerm) {
			// Add an operator
			if(!isTerm 
			//&& ExcludedCharacters.ToUpper().IndexOf(term.ToUpper()) > -1
			&& ExcludedCharacterList.Contains(term.ToUpper())) {
				this._searchTerms.Add(term.ToUpper());
			}
			// Add a new term
			else {
				switch(QueryType) {
					case QueryType.Difference:
						this._searchTerms.Add(string.Format("\'{0}\'", term.Replace("'", "''")));
						break;

					default:
						this._searchTerms.Add(string.Format("\'%{0}%\'", term.Replace("'", "''")));
						break;
				}
			}
		}

		#endregion

		#region "PROTECTED MEMBERS"
		// ---------------------------------------------------
		// PROTECTED MEMBERS
		// ---------------------------------------------------
		#endregion

		#region "PUBLIC MEMBERS"
		// ---------------------------------------------------
		// PUBLIC MEMBERS
		// ---------------------------------------------------

		/// <summary>
		/// Parses the specified search string.
		/// </summary>
		/// <param name="searchString">The search string.</param>
		/// <param name="fields">The fields.</param>
		/// <param name="queryOperator">The query operator.</param>
		/// <param name="queryType">Type of the query.</param>
		/// <returns></returns>
		public static string Parse(string searchString, string[] fields, QueryOperator queryOperator, QueryType queryType) {
			QueryParser parser = new QueryParser();
			parser.DefaultOperator = queryOperator;
			parser.QueryType = queryType;

			return parser.Parse(searchString, fields);
		}

		/// <summary>
		/// Parsing procedure
		/// </summary>
		/// <param name="searchString">Contains the user input, for example "Peace NOT War"</param>
		/// <param name="fields">String Array, defines the fields in which should be looked for in the database</param>
		/// <returns>Returns what has to be right to SELECT * FROM Table WHERE </returns>
		public string Parse(string searchString, string[] fields) {
			if(!string.IsNullOrEmpty(searchString)) {
				searchString += " ";
				bool inString = false;
				string tok = string.Empty;

				// Loop trough searchString and strip it
				for(int i = 0; i < searchString.Length; i++) {
					char character = searchString[i];

					switch(character) {
						case '"':
							inString = !inString;

							if(tok.Length > 0) {
								this._AddToSearchTerms(tok, !inString);
								tok = string.Empty;
							}
							break;

						case '(':
							if(!inString) {
								if(tok.Length > 0) {
									this._AddToSearchTerms(tok, false);
									tok = string.Empty;
								}

								this._AddToSearchTerms(character.ToString(), false);
							}
							else {
								tok += character.ToString();
							}
							break;

						case ')':
							goto case '(';

						default:
							if((inString || !character.Equals(' ')) && i + 1 < searchString.Length) {
								tok += character.ToString();
							}
							else {
								if(tok.Length > 0) {
									this._AddToSearchTerms(tok, false);
									tok = string.Empty;
								}
							}
							break;
					}
				}

				StringBuilder where = new StringBuilder(string.Empty);
				bool wasString = false;
				string defaultOperator = DefaultOperator.ToString();

				// Loop trough all SearchTerms items
				for(int i = 0; i < this._searchTerms.Count; i++) {
					if(ExcludedCharacters.IndexOf(this._searchTerms[i]) > -1) {
						// Prevent constructs like this one: WHERE AND NOT ...
						if(i > 0 
						|| this._searchTerms[i].ToUpper() != "AND") {
							if(this._searchTerms[i] == "(") {
								where.AppendFormat(" {0} ", defaultOperator);
							}

							// Add operator to statement
							if(this._searchTerms[i] == "NOT") {
								this._searchTerms[i] = "AND " + this._searchTerms[i];
							}

							where.AppendFormat(" {0} ", this._searchTerms[i]);

							if(this._searchTerms[i] == ")") {
								where.AppendFormat(" {0} ", defaultOperator);
							}

							wasString = false;
						}
					}
					else {
						// Add the default operator to link the single statements
						if(wasString) {
							where.AppendFormat(" {0} ", defaultOperator);
						}

						// Add the term for each database column (field).
						switch(QueryType) {
							case QueryType.Difference:
								where.Append("(");

								for(int y = 0; y < fields.Length; y++) {
									where.AppendFormat("DIFFERENCE({0},{1}) >= {2}", fields[y], this._searchTerms[i], MinDifference);
									
									if(y + 1 < fields.Length) {
										where.Append(" OR ");
									}
								}

								where.Append(")");
								break;

							default:
								where.AppendFormat(
									" ({0} LIKE {1}) ", 
									string.Join(
										string.Format(" LIKE {0} OR ", this._searchTerms[i]), 
										fields
									), 
									this._searchTerms[i]
								);
								break;
						}

						wasString = true;
					}
				}

				// Return the Where statement. Ready :-)
				return where.ToString();
			}
			else {
				// When searchString is empty, then return an empty string.
				return string.Empty;
			}
		}

		#endregion
	}
}
